﻿using System;
using System.Reflection.Emit;
using System.Windows.Input;
using System.Reflection;
using System.Windows;

namespace Monti.MVVM.Commands
{
   /// <summary>
   /// Defines the command behavior binding
   /// http://marlongrech.wordpress.com/2008/12/13/attachedcommandbehavior-v2-aka-acb/
   /// </summary>
   public class CommandBehaviorBinding : IDisposable
   {
      #region Fields

      private bool m_Disposed;

      #endregion

      #region Properties

      /// <summary>
      /// Get the owner of the CommandBinding ex: a Button
      /// This property can only be set from the BindEvent Method
      /// </summary>
      public DependencyObject Owner { get; private set; }

      /// <summary>
      /// The event name to hook up to
      /// This property can only be set from the BindEvent Method
      /// </summary>
      public string EventName { get; private set; }

      /// <summary>
      /// The event info of the event
      /// </summary>
      public EventInfo Event { get; private set; }

      /// <summary>
      /// Gets the EventHandler for the binding with the event
      /// </summary>
      public Delegate EventHandler { get; private set; }

      /// <summary>
      /// Gets or sets a CommandParameter
      /// </summary>
      public object CommandParameter { get; set; }

      /// <summary>
      /// The command to execute when the specified event is raised
      /// </summary>
      public ICommand Command { get; set; }

      /// <summary>
      /// Gets or sets the Action
      /// </summary>
      public Action <object> Action { get; set; }

      #endregion

      #region Methods

      /// <summary>
      /// Binds the event with the given name to the action and command.
      /// </summary>
      /// <param name="owner">Owner of evnet.</param>
      /// <param name="eventName">Name of event to bind.</param>
      public void BindEvent (DependencyObject owner, string eventName)
      {
         this.m_Disposed = false;
         this.EventName = eventName;
         this.Owner = owner;
         this.Event = Owner.GetType ().GetEvent (EventName, BindingFlags.Public | BindingFlags.Instance);
         if (this.Event == null)
            throw new InvalidOperationException (String.Format ("Could not resolve event name {0}", EventName));

         //Create an event handler for the event that will call the ExecuteCommand method
         this.EventHandler = CreateDelegate (
             Event.EventHandlerType, typeof (CommandBehaviorBinding).GetMethod ("Execute", BindingFlags.Public | BindingFlags.Instance), this);
         //Register the handler to the Event
         this.Event.AddEventHandler (Owner, EventHandler);
      }

      /// <summary>
      /// Executes the command and action.
      /// </summary>
      /// <param name="sender">Object triggering event.</param>
      public void Execute (object sender, __arglist)
      {
         var argIterator = new ArgIterator (__arglist);
         var eventArgs = TypedReference.ToObject(argIterator.GetNextArg());
         var eventArgsAsArgs = eventArgs as EventArgs;
         var data = new HandlerData (sender, this.CommandParameter, eventArgsAsArgs);
         if (eventArgsAsArgs == null) data.Tag = eventArgs;

         if (this.Command != null)
         {
            if (this.Command.CanExecute (data))
               this.Command.Execute (data);
         }
         if (this.Action != null)
            this.Action (data);
      }
      
      /// <summary>
      /// Generates a delegate with a matching signature of the supplied eventHandlerType
      /// This method only supports Events that have a delegate of type void
      /// </summary>
      /// <param name="eventHandlerType">The delegate type to wrap. Note that this must always be a void delegate</param>
      /// <param name="methodToInvoke">The method to invoke</param>
      /// <param name="methodInvoker">The object where the method resides</param>
      /// <returns>Returns a delegate with the same signature as eventHandlerType that calls the methodToInvoke inside</returns>
      private static Delegate CreateDelegate (Type eventHandlerType, MethodInfo methodToInvoke, object methodInvoker)
      {
         //Get the eventHandlerType signature
         var eventHandlerInfo = eventHandlerType.GetMethod ("Invoke");
         if (eventHandlerInfo.ReturnParameter != null)
         {
            Type returnType = eventHandlerInfo.ReturnParameter.ParameterType;
            if (returnType != typeof (void))
               throw new ApplicationException ("Delegate has a return type. This only supprts event handlers that are void");
         }

         ParameterInfo[] delegateParameters = eventHandlerInfo.GetParameters ();
         //Get the list of type of parameters. Please note that we do + 1 because we have to push the object where the method resides i.e methodInvoker parameter
         var hookupParameters = new Type[delegateParameters.Length + 1];
         var typeParameters = new Type[delegateParameters.Length];
         hookupParameters[0] = methodInvoker.GetType ();
         for (int i = 0; i < delegateParameters.Length; i++)
         {
            hookupParameters [i + 1] = delegateParameters [i].ParameterType;
            typeParameters [i] = delegateParameters[i].ParameterType;
         }

         var handler = new DynamicMethod ("", typeof (void),
             hookupParameters, typeof (CommandBehaviorBinding));

         var eventIl = handler.GetILGenerator ();

         //load the parameters or everything will just BAM :)
         var local = eventIl.DeclareLocal (typeof (object[]));
         eventIl.Emit (OpCodes.Ldc_I4, delegateParameters.Length + 1);
         eventIl.Emit (OpCodes.Newarr, typeof (object));
         eventIl.Emit (OpCodes.Stloc, local);

         //start from 1 because the first item is the instance. Load up all the arguments
         for (int i = 1; i < delegateParameters.Length + 1; i++)
         {
            eventIl.Emit (OpCodes.Ldloc, local);
            eventIl.Emit (OpCodes.Ldc_I4, i);
            eventIl.Emit (OpCodes.Ldarg, i);
            eventIl.Emit (OpCodes.Stelem_Ref);
         }

         eventIl.Emit (OpCodes.Ldloc, local);

         //Load as first argument the instance of the object for the methodToInvoke i.e methodInvoker
         eventIl.Emit (OpCodes.Ldarg_0);
         eventIl.Emit (OpCodes.Ldarg_1);
         eventIl.Emit (OpCodes.Ldarg_2);
         eventIl.Emit (OpCodes.Ldarg_3);

         //Now that we have it all set up call the actual method that we want to call for the binding
         eventIl.EmitCall (OpCodes.Call, methodToInvoke, typeParameters);

         eventIl.Emit (OpCodes.Pop);
         eventIl.Emit (OpCodes.Ret);

         //create a delegate from the dynamic method
         return handler.CreateDelegate (eventHandlerType, methodInvoker);
      }

      #region IDisposable Members

      /// <summary>
      /// Unregisters the EventHandler from the Event
      /// </summary>
      public void Dispose ()
      {
         if (!this.m_Disposed)
         {
            this.Event.RemoveEventHandler (Owner, EventHandler);
            this.m_Disposed = true;
         }
      }

      #endregion

      #endregion
   }
}
